/******************************************************************************
	Cobalt - utility libraries for building XNA games
	Copyright (C) 2008 Philip Djonov

	This program is free software; you can redistribute it and/or modify it
	under the terms of the GNU General Public License as published by the Free
	Software Foundation; either version 2 of the License, or (at your option)
	any later version.

	This program is distributed in the hope that it will be useful, but
	WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
	or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
	for more details.

	You should have received a copy of the GNU General Public License along
	with this program; if not, write to the
	
		Free Software Foundation, Inc.
		51 Franklin Street, Fifth Floor
		Boston, MA  02110-1301, USA.
******************************************************************************/

using System;
using System.ComponentModel;

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Content.Pipeline;
using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
using Microsoft.Xna.Framework.Content.Pipeline.Processors;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Graphics.PackedVector;

namespace Cobalt.Content.Pipeline.Graphics.Imaging
{
	[ContentProcessor( DisplayName = "Bump Map Generator" )]
	public class CreateBumpMapProcessor : ContentProcessor<TextureContent, TextureContent>
	{
		[DefaultValue( 1.5F )]
		public float Smoothing { get; set; }

		public CreateBumpMapProcessor()
		{
			Smoothing = 1.5F;
		}

		public override TextureContent Process( TextureContent input, ContentProcessorContext context )
		{
			Texture2DContent input2D = input as Texture2DContent;
			if( input2D == null )
				throw new InvalidContentException( "Can only process 2D texture content.", input.Identity );

			Texture2DContent ret = new Texture2DContent();

			foreach( BitmapContent srcImg in input2D.Mipmaps )
			{
				float[][] tmp = BumpHelpers.GetLuminance( srcImg );
				
				if( Smoothing > 0 )
					BumpHelpers.SmoothImage( tmp, Smoothing );

				PixelBitmapContent<Alpha8> dstImg = new PixelBitmapContent<Alpha8>( srcImg.Width, srcImg.Height );
				for( int y = 0; y < dstImg.Height; y++ )
					for( int x = 0; x < dstImg.Width; x++ )
						dstImg.SetPixel( x, y, new Alpha8( Helpers.Saturate( (tmp[x][y] - 0.15F) * 8 ) ) );

				ret.Mipmaps.Add( dstImg );	
			}

			ret.GenerateMipmaps( false );
			return ret;
		}
	}

	[ContentProcessor( DisplayName = "Bump Map Processor" )]
	public class BumpMapProcessor : ContentProcessor<TextureContent, TextureContent>
	{
		public override TextureContent Process( TextureContent input, ContentProcessorContext context )
		{
			Texture2DContent input2D = input as Texture2DContent;
			if( input2D == null )
				throw new InvalidContentException( "Can only process 2D texture content.", input.Identity );

			Texture2DContent ret = new Texture2DContent();

			foreach( BitmapContent srcImg in input2D.Mipmaps )
			{
				PixelBitmapContent<Alpha8> dstImg = new PixelBitmapContent<Alpha8>( srcImg.Width, srcImg.Height );

				if( srcImg.GetType() == typeof( PixelBitmapContent<Alpha8> ) )
					BitmapContent.Copy( srcImg, dstImg );
				else
				{
					float[][] tmp = BumpHelpers.GetLuminance( srcImg );
					for( int y = 0; y < dstImg.Height; y++ )
						for( int x = 0; x < dstImg.Width; x++ )
							dstImg.SetPixel( x, y, new Alpha8( tmp[x][y] ) );
				}
				
				ret.Mipmaps.Add( dstImg );
			}

			ret.GenerateMipmaps( false );
			return ret;
		}
	}

	[ContentProcessor( DisplayName = "Normal Map Generator" )] 
	public class CreateNormalMapProcessor : ContentProcessor<TextureContent, TextureContent>
	{
		private float heightScale = 1.0F / 16.0F;
		[DefaultValue( 1.0F / 16.0F )]
		public float HeightScale
		{
			get { return heightScale; }
			set { heightScale = value; }
		}

		private float smoothBand = 0.2F;
		[DefaultValue( 0.2F )]
		public float SmoothBand
		{
			get { return smoothBand; }
			set { smoothBand = value; }
		}

		private int kernelWidth = 5;
		[DefaultValue( 5 )]
		public int KernelWidth
		{
			get { return kernelWidth; }
			set
			{
				if( value < 3 || value > 9 || (value % 2) != 1 )
					throw new ArgumentException();

				kernelWidth = value;
			}
		}

		[DefaultValue( 1.5F )]
		public float Smoothing { get; set; }

		public CreateNormalMapProcessor()
		{
			Smoothing = 1.5F;
		}


		private TextureAddressMode wrapMode = TextureAddressMode.Wrap;
		[DefaultValue( TextureAddressMode.Wrap )]
		public TextureAddressMode WrapMode
		{
			get { return wrapMode; }
			set
			{
				switch( value )
				{
				case TextureAddressMode.Wrap:
				case TextureAddressMode.Mirror:
				case TextureAddressMode.Clamp:
					break;

				default:
					throw new ArgumentException();
				}

				wrapMode = value;
			}
		}

		public override TextureContent Process( TextureContent input, ContentProcessorContext context )
		{
			Texture2DContent input2D = input as Texture2DContent;
			if( input2D == null )
				throw new InvalidContentException( "Can only process 2D texture content.", input.Identity );

			Texture2DContent ret = new Texture2DContent();

			Kernel2D kdu = new Kernel2D( kernelWidth, KernelType.Sobel );
			kdu.Normalize();
			Kernel2D kdv = kdu.Clone();
			kdv.Transpose();

			foreach( BitmapContent srcImg in input2D.Mipmaps )
			{
				float[][] tmp = BumpHelpers.GetLuminance( srcImg );

				if( Smoothing > 0 )
					BumpHelpers.SmoothImage( tmp, Smoothing );

				PixelBitmapContent<NormalizedByte4> dstImg = new PixelBitmapContent<NormalizedByte4>( srcImg.Width, srcImg.Height );

				for( int y = 0; y < dstImg.Height; y++ )
				{
					for( int x = 0; x < dstImg.Width; x++ )
					{
						float du = kdu.FilterImageData( tmp, x, y, wrapMode );
						float dv = kdv.FilterImageData( tmp, x, y, wrapMode );

						Vector3 n = new Vector3( du, dv, heightScale );
						n = Vector3.Normalize( n );

						dstImg.SetPixel( x, y, new NormalizedByte4( n.X, n.Y, n.Z, 0 ) );												
					}
				}

				ret.Mipmaps.Add( dstImg );
			}

			ret.GenerateMipmaps( false );
			return ret;
		}
	}

	internal static class BumpHelpers
	{
		public static float[][] GetLuminance( BitmapContent bmp )
		{
			float[][] ret = Helpers.CreateSquareArray<float>( bmp.Width, bmp.Height );

			if( bmp is PixelBitmapContent<Alpha8> )
			{
				PixelBitmapContent<Alpha8> aImg = bmp as PixelBitmapContent<Alpha8>;

				for( int y = 0; y < aImg.Height; y++ )
				{
					for( int x = 0; x < aImg.Width; x++ )
					{
						ret[x][y] = aImg.GetPixel( x, y ).ToAlpha();
					}
				}
			}
			else
			{
				PixelBitmapContent<Vector4> tmp;
				if( bmp is PixelBitmapContent<Vector4> )
				{
					tmp = bmp as PixelBitmapContent<Vector4>;
				}
				else
				{
					tmp = new PixelBitmapContent<Vector4>( bmp.Width, bmp.Height );
					BitmapContent.Copy( bmp, tmp );
				}

				for( int y = 0; y < tmp.Height; y++ )
				{
					for( int x = 0; x < tmp.Width; x++ )
					{
						Vector4 inPix = tmp.GetPixel( x, y );
						float lum = Helpers.Saturate( inPix.X * 0.3F + inPix.Y * 0.59F + inPix.Z * 0.11F );
						ret[x][y] = lum;
					}
				}
			}

			return ret;
		}

		public static void SmoothImage( float[][] image, float strength )
		{
			float[][] img2 = Helpers.CreateSquareArray<float>( image[0].Length, image.Length );

			float[] gausWeights = new float[4];
			for( int i = 0; i < gausWeights.Length; i++ )
				gausWeights[i] = GetGaussianWeight( i, strength );

			float sum = gausWeights[0];
			for( int i = 1; i < gausWeights.Length; i++ )
				sum += gausWeights[i] * 2;

			float div = 1.0F / sum;

			for( int i = 0; i < gausWeights.Length; i++ )
				gausWeights[i] *= div;

			FilterGaus1D( img2, image, gausWeights );
			FilterGaus1D( image, img2, gausWeights );
		}

		private static float GetGaussianWeight( float n, float blurStrength )
		{
			return (float)((1.0 / Math.Sqrt( 2 * Math.PI * blurStrength )) *
						   Math.Exp( -(n * n) / (2 * blurStrength * blurStrength) ));
		}

		private static void FilterGaus1D( float[][] dest, float[][] source, float[] gausWeights )
		{
			for( int x = 0; x < source.Length; x++ )
			{
				float[] line = source[x];
				for( int y = 0; y < line.Length; y++ )
				{
					float sum = line[y] * gausWeights[0];

					for( int i = 1; i < gausWeights.Length; i++ )
					{
						int t;
							
						t = y - i;					
						Wrap( ref t, line.Length, TextureAddressMode.Wrap );
						sum += line[t] * gausWeights[i];

						t = y + i;
						Wrap( ref t, line.Length, TextureAddressMode.Wrap );
						sum += line[t] * gausWeights[i];
					}

					dest[y][x] = sum;
				}
			}
		}

		private static void Wrap( ref int x, int width, TextureAddressMode wrapMode )
		{
			switch( wrapMode )
			{
				case TextureAddressMode.Clamp:
					x = Helpers.Clamp( x, 0, width - 1 );
					break;

				case TextureAddressMode.Wrap:
					x = x >= 0 ? x % width : (x + 1) % width + width - 1;
					break;

				case TextureAddressMode.Mirror:
					x = Math.Abs( x );
					while( x >= width )
						x = width + width - x - 2;
					break;

				default:
					throw new ArgumentException();
			}
		}

		public static void AutoLevelLuminance( float[][] image, float smoothBand )
		{
			float totalLum = 0;
			for( int x = 0; x < image.Length; x++ )
			{
				float[] col = image[x];
				for( int y = 0; y < col.Length; y++ )
					totalLum += col[y];
			}

			float avgLum = totalLum / (image.Length * image[0].Length);
			float aMin = Helpers.Saturate( avgLum - smoothBand );
			float aMax = Helpers.Saturate( avgLum + smoothBand );

			for( int x = 0; x < image.Length; x++ )
			{
				float[] col = image[x];
				for( int y = 0; y < col.Length; y++ )
					col[y] = MathHelper.SmoothStep( aMin, aMax, col[y] );
			}
		}

		public static void AutoEdgeFixup( float[][] image )
		{
			float minVal = 1;

			float[] col;

			col = image[0];
			for( int y = 0; y < col.Length; y++ )
				minVal = Math.Min( minVal, col[y] );
			col = image[image.Length - 1];
			for( int y = 0; y < col.Length; y++ )
				minVal = Math.Min( minVal, col[y] );

			for( int x = 1; x < image.Length - 1; x++ )
			{
				col = image[x];
				minVal = Math.Min( minVal, col[0] );
				minVal = Math.Min( minVal, col[col.Length - 1] );
			}

			float bias = 1.0F - minVal;

			for( int x = 0; x < image.Length; x++ )
			{
				col = image[x];
				for( int y = 0; y < col.Length; y++ )
					col[y] = Helpers.Saturate( col[y] + bias );
			}
		}
	}
}